package cnisetup

import (
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strings"

	f5_bigip "github.com/f5devcentral/f5-bigip-rest-go/bigip"
	"github.com/f5devcentral/f5-bigip-rest-go/utils"
	v1 "k8s.io/api/core/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/manager"
)

func (cnictx *CNIContext) Dump() string {
	slog := utils.LogFromContext(cnictx.Context)
	// fmt.Printf("%#v\n", config)
	if bcs, err := json.MarshalIndent(cnictx.CNIConfig, "", "  "); err != nil {
		slog.Warnf("failed to show the parsed configs: %s", err.Error())
		return ""
	} else {
		return string(bcs)
	}
}

func (cnictx *CNIContext) Convert() (map[string]interface{}, error) {
	ncfgs := map[string]interface{}{}
	c := &cnictx.CNIConfig
	// slog := utils.LogFromContext(cnictx)

	for _, selfip := range c.SelfIPs {
		// strange that tunnel's selfip can be created before tunnel creation.
		ncfgs["net/self/"+selfip.Name] = parseSelf(selfip.Name, selfip.IpMask, selfip.VlanOrTunnelName)
	}

	for _, vlan := range c.Vlans {
		ncfgs["net/vlan/"+vlan.Name] = parseVlan(vlan.Name, vlan.Tag, vlan.Interfaces)
	}

	for _, tunnel := range c.Tunnels {
		ncfgs["net/tunnels/vxlan/"+tunnel.Name] = parseVxlanProfile(tunnel.Name, tunnel.Port, tunnel.FloodingType)
		ncfgs["net/tunnels/tunnel/"+tunnel.Name] = parseTunnel(tunnel.Name, tunnel.Key, tunnel.LocalAddress, tunnel.Name)
	}

	for _, bgp := range c.BGPs {
		ncfgs["net/routing/bgp/"+bgp.Name] = parseBGP(bgp.Name, bgp.LocalAS, bgp.RemoteAS)
	}

	for _, route := range c.Routes {
		rn := strings.Split(route.Network, "/")
		ncfgs["net/route/"+rn[0]] = parseRoute(route.Network, route.TmInterface)
	}

	return ncfgs, nil
}

// ConvertSubs will convert all resources based on k8s nodes
func (cnictx *CNIContext) ConvertSubs() (map[string]interface{}, error) {
	rlt := map[string]interface{}{}
	for _, tunnel := range cnictx.Tunnels {
		for _, kname := range tunnel.FdbFrom {
			if kset, f := cnictx.Kubes[kname]; f {
				k8scni := cnictx.NewK8SCNI(kset)
				c, err := k8scni.nodeConfigs()
				if err != nil {
					return nil, err
				}
				for k, v := range c {
					rlt[k] = v
				}
			}
		}
	}
	for _, bgp := range cnictx.BGPs {
		for _, kname := range bgp.NeighborFrom {
			if kset, f := cnictx.Kubes[kname]; f {
				k8scni := cnictx.NewK8SCNI(kset)
				c, err := k8scni.nodeConfigs()
				if err != nil {
					return nil, err
				}
				for k, v := range c {
					rlt[k] = v
				}
			}
		}
	}
	return rlt, nil
}

// GatherSubs will gather all existing sub-resources from bigip.
func (cnictx *CNIContext) GatherSubs() (map[string]interface{}, error) {
	ncfgs := map[string]interface{}{}
	for _, tunnel := range cnictx.Tunnels {
		fdbs, err := cnictx.getTunnelFdbs(tunnel.Name)
		if err != nil {
			return nil, err
		} else {
			for _, fdb := range fdbs {
				m := fdb.(map[string]interface{})["name"]
				name := fmt.Sprintf("net/fdb/tunnel/~%s~%s/records/%s", "Common", tunnel.Name, m)
				ncfgs[name] = fdb
			}
		}
	}

	for _, bgp := range cnictx.BGPs {
		neighbors, err := cnictx.getBGPNeighbors(bgp.Name)
		if err != nil {
			return nil, err
		} else {
			for _, nbr := range neighbors {
				m := nbr.(map[string]interface{})["name"]
				name := fmt.Sprintf("net/routing/bgp/~%s~%s/neighbor/%s", "Common", bgp.Name, m)
				ncfgs[name] = nbr
			}
		}
	}
	return ncfgs, nil
}

func (cnictx *CNIContext) Deploy() error {
	// TODO: optimize bc generation, not create bc every time.
	bc := cnictx.NewBC()
	if len(cnictx.BGPs) > 0 {
		if err := enableBGPRouting(bc); err != nil {
			return err
		}
	}

	if err := cnictx.deploy(); err != nil {
		return err
	}

	for i := range cnictx.Tunnels {
		if mac, err := macAddrOfTunnel(bc, cnictx.Tunnels[i].Name); err != nil {
			return err
		} else {
			cnictx.Tunnels[i].tunnelMac = mac
		}
	}

	return nil
}

func (cnictx *CNIContext) Cleanup() error {
	// clean arp, to avoid the following error
	// 	{"code":400,"message":"transaction failed:01071d9d:3:
	//	Neighbor entry (10.244.2.5) can not be resolved, due to
	// 	update/delete of self-ip (10.244.20.1/255.255.0.0).","errorStack":[],"apiError":2}
	if err := cnictx.cleanNetArps(); err != nil {
		return err
	}

	// {"code":400,"message":"transaction failed:01070604:3:
	// Cannot delete IP 172.18.0.200 because it would leave a route unreachable.",
	// "errorStack":[],"apiError":2}
	if err := cnictx.cleanRoutes(); err != nil {
		return err
	}

	return cnictx.cleanup()
}

func (cnictx *CNIContext) deploy() error {
	bc := cnictx.NewBC()
	ncfgs := map[string]interface{}{}

	if cfgs, err := cnictx.Convert(); err != nil {
		return err
	} else {
		for k, v := range cfgs {
			ncfgs[k] = v
		}
	}

	if cfgs, err := cnictx.ConvertSubs(); err != nil {
		return err
	} else {
		for k, v := range cfgs {
			ncfgs[k] = v
		}
	}
	if err := deploy(bc, "Common", nil, &map[string]interface{}{"": ncfgs}); err != nil {
		return err
	}

	return nil
}

func (cnictx *CNIContext) cleanup() error {
	ncfgs, err := cnictx.Convert()
	if err != nil {
		return err
	}

	if cfgs, err := cnictx.GatherSubs(); err != nil {
		return err
	} else {
		for k, v := range cfgs {
			ncfgs[k] = v
		}
	}

	bc := cnictx.NewBC()
	err = deploy(bc, "Common", &map[string]interface{}{"": ncfgs}, nil)
	if err != nil {
		return err
	}

	return nil
}

func (cnictx *CNIContext) NewBC() *f5_bigip.BIGIPContext {
	mgmt := cnictx.Mgmt
	url := fmt.Sprintf("https://%s:%d", mgmt.IpAddress, *mgmt.Port)
	bigip := f5_bigip.New(url, mgmt.Username, mgmt.Password)
	return &f5_bigip.BIGIPContext{BIGIP: *bigip, Context: cnictx.Context}
}

func (cnictx *CNIContext) NewK8SCNI(kube KubeSetting) K8SCNI {
	slog := utils.LogFromContext(cnictx)
	switch kube.CNIType {
	case CNIType_Calico_bgp:
		return &CalicoBGPContext{CNIContext: *cnictx, Kube: kube}
	case CNIType_Flannel_vxlan:
		return &FlannelVxlanContext{CNIContext: *cnictx, Kube: kube}
	case CNIType_Flannel_host_gw:
		return &FlannelHostgwContext{CNIContext: *cnictx, Kube: kube}
	case CNIType_Cilium:
		return &CiliumContext{CNIContext: *cnictx, Kube: kube}
	case CNIType_BIGIP_only:
		return &BIGIPOnlyContext{CNIContext: *cnictx, Kube: kube}
	default:
		slog.Errorf("not support CNI type: %s", kube.CNIType)
		return &UnknownContext{CNIContext: *cnictx, Kube: kube}
	}
}

func (cnictx *CNIContext) getTunnelFdbs(name string) ([]interface{}, error) {
	ep := fmt.Sprintf("net/fdb/tunnel/~%s~%s/records", "Common", name)
	bc := cnictx.NewBC()
	existings, err := bc.All(ep)
	if err != nil {
		matched, e := regexp.MatchString(`.*was not found\..*`, err.Error())
		if e != nil {
			return nil, errors.Join(err, e)
		} else if matched {
			return nil, nil
		} else {
			return nil, err
		}
	} else if existings != nil {
		if items, f := (*existings)["items"]; f {
			return items.([]interface{}), nil
		} else {
			return nil, fmt.Errorf("not 'items' key found")
		}
	} else {
		return nil, nil
	}
}

func (cnictx *CNIContext) getBGPNeighbors(name string) ([]interface{}, error) {
	ep := fmt.Sprintf("net/routing/bgp/~%s~%s/neighbor", "Common", name)
	bc := cnictx.NewBC()
	existings, err := bc.All(ep)
	if err != nil {
		matched, e := regexp.MatchString(`.*was not found\..*`, err.Error())
		if e != nil {
			return nil, errors.Join(err, e)
		} else if matched {
			return nil, nil
		} else {
			return nil, err
		}
	} else if existings != nil {
		if items, f := (*existings)["items"]; f {
			return items.([]interface{}), nil
		} else {
			return nil, fmt.Errorf("not 'items' key found")
		}
	} else {
		return nil, nil
	}
}

func (cnictx *CNIContext) OnTrace(mgr manager.Manager, loglevel string) error {
	rNode := &NodeReconciler{
		Client:   mgr.GetClient(),
		Scheme:   mgr.GetScheme(),
		LogLevel: loglevel,
		CNIConf:  &cnictx.CNIConfig,
	}
	err := ctrl.NewControllerManagedBy(mgr).For(&v1.Node{}).Complete(rNode)
	if err != nil {
		return err
	}
	return nil
}

func (cnictx *CNIContext) searchTunnelMacWith(localAddr string) (string, error) {
	for _, tunnel := range cnictx.Tunnels {
		if tunnel.LocalAddress == localAddr {
			return tunnel.tunnelMac, nil
		}
	}
	return "", fmt.Errorf("no tunnel with IP address '%s' found in the config", localAddr)
}

func (cnictx *CNIContext) cleanNetArps() error {
	bc := cnictx.NewBC()
	arpsp, err := bc.All("net/arp")
	if err != nil {
		return err
	}
	arps := map[string]map[string]map[string]interface{}{}
	items := (*arpsp)["items"].([]interface{})
	for _, i := range items {
		mi := i.(map[string]interface{})
		if fullPath, f := mi["fullPath"]; f {
			fp := strings.Split(fullPath.(string), "/")
			pt, sf, nm := "", "", ""
			if len(fp) == 3 {
				pt, nm = fp[1], fp[2]
			} else {
				pt, sf, nm = fp[1], fp[2], fp[3]
			}
			if _, f1 := arps[pt]; !f1 {
				arps[pt] = map[string]map[string]interface{}{}
			}
			if _, f2 := arps[pt][sf]; !f2 {
				arps[pt][sf] = map[string]interface{}{}
			}
			if _, f3 := arps[pt][sf][nm]; !f3 {
				arps[pt][sf]["net/arp/"+nm] = mi
			}
		}
	}

	for k, v := range arps {
		b, _ := json.Marshal(v)
		var fdrs map[string]interface{}
		json.Unmarshal(b, &fdrs)
		if err := deploy(bc, k, &fdrs, nil); err != nil {
			return err
		}
	}
	return nil
}

func (cnictx *CNIContext) cleanRoutes() error {
	bc := cnictx.NewBC()
	arpsp, err := bc.All("net/route")
	if err != nil {
		return err
	}
	arps := map[string]map[string]map[string]interface{}{}
	items := (*arpsp)["items"].([]interface{})
	for _, i := range items {
		mi := i.(map[string]interface{})
		if fullPath, f := mi["fullPath"]; f {
			fp := strings.Split(fullPath.(string), "/")
			pt, sf, nm := "", "", ""
			if len(fp) == 3 {
				pt, nm = fp[1], fp[2]
			} else {
				pt, sf, nm = fp[1], fp[2], fp[3]
			}
			if _, f1 := arps[pt]; !f1 {
				arps[pt] = map[string]map[string]interface{}{}
			}
			if _, f2 := arps[pt][sf]; !f2 {
				arps[pt][sf] = map[string]interface{}{}
			}
			if _, f3 := arps[pt][sf][nm]; !f3 {
				arps[pt][sf]["net/route/"+nm] = mi
			}
		}
	}

	for k, v := range arps {
		b, _ := json.Marshal(v)
		var fdrs map[string]interface{}
		json.Unmarshal(b, &fdrs)
		if err := deploy(bc, k, &fdrs, nil); err != nil {
			return err
		}
	}
	return nil
}
